;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC

(ns app.main.ui.static
  (:require-macros [app.main.style :as stl])
  (:require
   ["rxjs" :as rxjs]
   [app.common.data :as d]
   [app.common.pprint :as pp]
   [app.common.uri :as u]
   [app.common.uuid :as uuid]
   [app.config :as cf]
   [app.main.data.auth :refer [is-authenticated?]]
   [app.main.data.common :as dcm]
   [app.main.data.event :as ev]
   [app.main.errors :as errors]
   [app.main.refs :as refs]
   [app.main.repo :as rp]
   [app.main.router :as rt]
   [app.main.store :as st]
   [app.main.ui.auth.login :refer [login-dialog*]]
   [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
   [app.main.ui.auth.register :as register]
   [app.main.ui.dashboard.sidebar :refer [sidebar*]]
   [app.main.ui.ds.buttons.button :refer [button*]]
   [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
   [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
   [app.main.ui.icons :as deprecated-icon]
   [app.main.ui.viewer.header :as viewer.header]
   [app.util.dom :as dom]
   [app.util.i18n :refer [tr]]
   [app.util.timers :as tm]
   [app.util.webapi :as wapi]
   [beicon.v2.core :as rx]
   [cuerdas.core :as str]
   [potok.v2.core :as ptk]
   [rumext.v2 :as mf]))

;; FIXME: this is a workaround until we export this class on beicon library
(def TimeoutError rxjs/TimeoutError)

(mf/defc error-container*
  {::mf/props :obj}
  [{:keys [children]}]
  (let [profile-id  (:profile-id @st/state)
        on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
    [:section {:class (stl/css :exception-layout)}
     [:button
      {:class (stl/css :exception-header)
       :on-click on-nav-root}
      [:> raw-svg* {:id "penpot-logo-icon" :class (stl/css :penpot-logo)}]
      (when profile-id
        [:div {:class (stl/css :go-back-wrapper)}
         [:> icon* {:icon-id i/arrow :class (stl/css :back-arrow)}] [:span (tr "not-found.no-permission.go-dashboard")]])]
     [:div {:class (stl/css :deco-before)} deprecated-icon/logo-error-screen]
     (when-not profile-id
       [:button {:class (stl/css :login-header)
                 :on-click on-nav-root}
        (tr "labels.login")])

     [:div {:class (stl/css :exception-content)}
      [:div {:class (stl/css :container)} children]]

     [:div {:class (stl/css :deco-after2)}
      [:span (tr "labels.copyright-period")]
      deprecated-icon/logo-error-screen
      [:span (tr "not-found.made-with-love")]]]))

(mf/defc invalid-token
  []
  [:> error-container* {}
   [:div {:class (stl/css :main-message)} (tr "errors.invite-invalid")]
   [:div {:class (stl/css :desc-message)} (tr "errors.invite-invalid.info")]])

(mf/defc login-modal*
  {::mf/private true}
  []
  (let [current-section  (mf/use-state :login)
        user-email       (mf/use-state "")
        register-token   (mf/use-state "")

        set-section
        (mf/use-fn
         (fn [event]
           (let [section (-> (dom/get-current-target event)
                             (dom/get-data "section")
                             (keyword))]
             (reset! current-section section))))

        set-section-recovery
        (mf/use-fn
         #(reset! current-section :recovery-request))

        set-section-login
        (mf/use-fn
         #(reset! current-section :login))

        success-login
        (mf/use-fn
         #(st/emit! (rt/reload true)))

        success-register
        (mf/use-fn
         (fn [data]
           (reset! register-token (:token data))
           (reset! current-section :register-validate)))

        register-email-sent
        (mf/use-fn
         (fn [email]
           (reset! user-email email)
           (reset! current-section :register-email-sent)))

        recovery-email-sent
        (mf/use-fn
         (fn [email]
           (reset! user-email email)
           (reset! current-section :recovery-email-sent)))

        on-nav-root
        (mf/use-fn #(st/emit! (rt/nav :auth-login {})))]

    [:div {:class (stl/css :overlay)}
     [:div {:class (stl/css :dialog-login)}
      [:div {:class (stl/css :modal-close)}
       [:button {:class (stl/css :modal-close-button)
                 :on-click on-nav-root}
        deprecated-icon/close]]
      [:div {:class (stl/css :login)}
       [:div {:class (stl/css :logo)} deprecated-icon/logo]

       (case @current-section
         :login
         [:*
          [:div {:class (stl/css :logo-title)} (tr "labels.login")]
          [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.free")]
          [:> login-dialog* {:on-recovery-request set-section-recovery
                             :on-success-callback success-login
                             :handle-redirect true}]
          [:hr {:class (stl/css :separator)}]
          [:div {:class (stl/css :change-section)}
           (tr "auth.register")
           " "
           [:a {:data-section "register"
                :on-click set-section}
            (tr "auth.register-submit")]]]

         :register
         [:*
          [:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
          [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
          [:& register/register-methods {:on-success-callback success-register :hide-separator true}]
          #_[:hr {:class (stl/css :separator)}]
          [:div {:class (stl/css :separator)}]
          [:div {:class (stl/css :change-section)}
           (tr "auth.already-have-account")
           " "
           [:a {:data-section "login"
                :on-click set-section} (tr "auth.login-here")]]
          [:div {:class (stl/css :links)}
           [:hr {:class (stl/css :separator)}]
           [:& register/terms-register]]]

         :register-validate
         [:div {:class (stl/css :form-container)}
          [:& register/register-form
           {:params {:token @register-token}
            :on-success-callback register-email-sent}]
          [:div {:class (stl/css :links)}
           [:div {:class (stl/css :register)}
            [:a {:data-section "register"
                 :on-click set-section}
             (tr "labels.go-back")]]]]

         :register-email-sent
         [:div {:class (stl/css :form-container)}
          [:& register/register-success-page {:params {:email @user-email :hide-logo true}}]]

         :recovery-request
         [:& recovery-request-page {:go-back-callback set-section-login
                                    :on-success-callback recovery-email-sent}]

         :recovery-email-sent
         [:div {:class (stl/css :form-container)}
          [:& recovery-sent-page {:email @user-email}]])]]]))

(mf/defc request-dialog*
  {::mf/props :obj}
  [{:keys [title content button-text on-button-click cancel-text on-close]}]
  (let [on-click (or on-button-click on-close)]
    [:div {:class (stl/css :overlay)}
     [:div {:class (stl/css :dialog)}
      [:div {:class (stl/css :modal-close)}
       [:button {:class (stl/css :modal-close-button) :on-click on-close}
        deprecated-icon/close]]
      [:div {:class (stl/css :dialog-title)} title]
      (for [[index content] (d/enumerate content)]
        [:div {:key index} content])
      [:div {:class (stl/css :sign-info)}
       (when cancel-text
         [:button {:class (stl/css :cancel-button)
                   :on-click on-close}
          cancel-text])
       [:button {:on-click on-click} button-text]]]]))

(mf/defc request-access*
  [{:keys [file-id team-id is-default is-workspace profile]}]
  (let [requested*  (mf/use-state {:sent false :already-requested false})
        requested   (deref requested*)

        on-close
        (mf/use-fn
         (mf/deps profile)
         (fn []
           (let [team-id (:default-team-id profile)]
             (st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))

        on-success
        (mf/use-fn
         #(reset! requested* {:sent true :already-requested false}))

        on-error
        (mf/use-fn
         #(reset! requested* {:sent true :already-requested true}))

        on-request-access
        (mf/use-fn
         (mf/deps file-id team-id is-workspace)
         (fn []
           (let [params (if (some? file-id)
                          {:file-id file-id
                           :is-viewer (not is-workspace)}
                          {:team-id team-id})
                 mdata  {:on-success on-success
                         :on-error on-error}]
             (st/emit! (dcm/create-team-access-request
                        (with-meta params mdata))))))]

    (cond
      is-default
      [:> request-dialog* {:title (tr "not-found.no-permission.project")
                           :button-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}]

      (and (some? file-id) (:already-requested requested))
      [:> request-dialog* {:title (tr "not-found.no-permission.already-requested.file")
                           :content [(tr "not-found.no-permission.already-requested.or-others.file")]
                           :button-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}]

      (:already-requested requested)
      [:> request-dialog* {:title (tr "not-found.no-permission.already-requested.project")
                           :content [(tr "not-found.no-permission.already-requested.or-others.project")]
                           :button-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}]

      (:sent requested)
      [:> request-dialog* {:title (tr "not-found.no-permission.done.success")
                           :content [(tr "not-found.no-permission.done.remember")]
                           :button-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}]

      (some? file-id)
      [:> request-dialog* {:title (tr "not-found.no-permission.file")
                           :content [(tr "not-found.no-permission.you-can-ask.file")
                                     (tr "not-found.no-permission.if-approves")]
                           :button-text (tr "not-found.no-permission.ask")
                           :on-button-click on-request-access
                           :cancel-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}]

      (some? team-id)
      [:> request-dialog* {:title (tr "not-found.no-permission.project")
                           :content [(tr "not-found.no-permission.you-can-ask.project")
                                     (tr "not-found.no-permission.if-approves")]
                           :button-text (tr "not-found.no-permission.ask")
                           :on-button-click on-request-access
                           :cancel-text (tr "not-found.no-permission.go-dashboard")
                           :on-close on-close}])))

(mf/defc not-found*
  []
  [:> error-container* {}
   [:div {:class (stl/css :main-message)} (tr "labels.not-found.main-message")]
   [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.error")]
   [:div {:class (stl/css :desc-message)} (tr "not-found.desc-message.doesnt-exist")]])

(mf/defc bad-gateway*
  []
  (let [handle-retry
        (mf/use-fn
         (fn [] (st/emit! (rt/assign-exception nil))))]
    [:> error-container* {}
     [:div {:class (stl/css :main-message)} (tr "labels.bad-gateway.main-message")]
     [:div {:class (stl/css :desc-message)} (tr "labels.bad-gateway.desc-message")]
     [:div {:class (stl/css :sign-info)}
      [:button {:on-click handle-retry} (tr "labels.retry")]]]))

(mf/defc service-unavailable*
  []
  (let [on-click (mf/use-fn #(st/emit! (rt/assign-exception nil)))]
    [:> error-container* {}
     [:div {:class (stl/css :main-message)} (tr "labels.service-unavailable.main-message")]
     [:div {:class (stl/css :desc-message)} (tr "labels.service-unavailable.desc-message")]
     [:div {:class (stl/css :sign-info)}
      [:button {:on-click on-click} (tr "labels.retry")]]]))

(defn- generate-report
  [data]
  (try
    (let [team-id    (:current-team-id @st/state)
          profile-id (:profile-id @st/state)

          trace      (:app.main.errors/trace data)
          instance   (:app.main.errors/instance data)]
      (with-out-str
        (println "Hint:    " (or (:hint data) (ex-message instance) "--"))
        (println "Prof ID: " (str (or profile-id "--")))
        (println "Team ID: " (str (or team-id "--")))
        (println "URI:     " cf/public-uri)

        (when-let [file-id (:file-id data)]
          (println "File ID:" (str file-id)))

        (println)

        (println "Data:")
        (loop [data data]
          (-> (d/without-qualified data)
              (dissoc :explain)
              (d/update-when :data (constantly "(...)"))
              (pp/pprint {:level 8 :length 10}))

          (println)

          (when-let [explain (:explain data)]
            (print explain))

          (when (and (= :server-error (:type data))
                     (contains? data :data))
            (recur (:data data))))

        (println "Trace:")
        (println trace)
        (println)

        (println "Last events:")
        (pp/pprint @st/last-events {:length 200})

        (println)))
    (catch :default cause
      (.error js/console "error on generating report.txt" cause)
      nil)))

(mf/defc internal-error*
  [{:keys [on-reset report] :as props}]
  (let [report-uri (mf/use-ref nil)
        on-reset   (or on-reset #(st/emit! (rt/assign-exception nil)))

        support-contact-click
        (mf/use-fn
         (mf/deps on-reset report)
         (fn []
           (tm/schedule on-reset)
           (let [error-report-id (uuid/next)
                 error-href (rt/get-current-href)]
             (set! errors/last-report {:id error-report-id :content report})
             (st/emit!
              (rt/nav :settings-feedback {:type "issue"
                                          :error-report-id error-report-id
                                          :error-href error-href})))))

        on-download
        (mf/use-fn
         (fn [event]
           (dom/prevent-default event)
           (when-let [uri (mf/ref-val report-uri)]
             (dom/trigger-download-uri "report" "text/plain" uri))))]

    (mf/with-effect [report]
      (when (some? report)
        (set! errors/last-report report)
        (let [report (wapi/create-blob report "text/plain")
              uri    (wapi/create-uri report)]
          (mf/set-ref-val! report-uri uri)
          (fn []
            (wapi/revoke-uri uri)))))

    [:> error-container* {}
     [:div {:class (stl/css :main-message)} (tr "labels.internal-error.main-message")]

     [:div {:class (stl/css :desc-message)}
      [:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-first")]
      [:p {:class (stl/css :desc-text)} (tr "labels.internal-error.desc-message-second")]]

     (when (some? report)
       [:a {:class (stl/css :download-link) :on-click on-download} (tr "labels.download" "report.txt")])

     [:div {:class (stl/css :buttons-container)}
      [:> button* {:variant "secondary"
                   :type "button"
                   :class (stl/css :support-btn)
                   :on-click support-contact-click} (tr "labels.contact-support")]
      [:> button* {:variant "primary"
                   :type "button"
                   :class (stl/css :retry-btn)
                   :on-click on-reset} (tr "labels.retry")]]]))

(defn- load-info
  "Load exception page info"
  [path-params]
  (let [default {:loaded true}
        stream  (cond
                  (:file-id path-params)
                  (->> (rp/cmd! :get-file-info {:id (:file-id path-params)})
                       (rx/map (fn [info]
                                 {:loaded true
                                  :file-id (:id info)})))

                  (:team-id path-params)
                  (->> (rp/cmd! :get-team-info {:id (:team-id path-params)})
                       (rx/map (fn [info]
                                 {:loaded true
                                  :team-id (:id info)
                                  :team-default (:is-default info)})))

                  :else
                  (rx/of default))]

    (->> stream
         (rx/timeout 3000)
         (rx/catch (fn [cause]
                     (if (instance? TimeoutError cause)
                       (rx/of default)
                       (rx/throw cause)))))))

(mf/defc exception-section*
  {::mf/private true}
  [{:keys [data route] :as props}]
  (let [type   (get data :type)
        report (mf/with-memo [data]
                 (generate-report data))
        props  (mf/spread-props props {:report report})]

    (mf/with-effect [data route report]
      (let [params (:query-params route)
            params (u/map->query-string params)]
        (st/emit! (ptk/data-event ::ev/event
                                  {::ev/name "exception-page"
                                   :type (get data :type :unknown)
                                   :hint (get data :hint)
                                   :path (get route :path)
                                   :report report
                                   :params params}))))

    (case type
      :not-found
      [:> not-found* {}]

      :authentication
      [:> not-found* {}]

      :bad-gateway
      [:> bad-gateway* props]

      :service-unavailable
      [:> service-unavailable*]

      [:> internal-error* props])))

(mf/defc context-wrapper*
  [{:keys [is-workspace is-dashboard is-viewer profile children]}]
  [:*
   (cond
     is-workspace
     [:div {:class (stl/css :workspace)}
      [:div {:class (stl/css :workspace-left)}
       deprecated-icon/logo-icon
       [:div
        [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
        [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
      [:div {:class (stl/css :workspace-right)}]]

     is-viewer
     [:div {:class (stl/css :viewer)}
      ;; FIXME: the viewer header was never designed to be reused
      ;; from other parts of the application, and this code looks
      ;; like a fast workaround reusing it as-is without a proper
      ;; component adaptation for be able to use it easily it on
      ;; viewer context or static error page context
      [:& viewer.header/header {:project
                                {:name (tr "not-found.no-permission.project-name")}
                                :index 0
                                :file {:name (tr "not-found.no-permission.penpot-file")}
                                :page nil
                                :frame nil
                                :permissions {:is-logged true}
                                :zoom 1
                                :section :interactions
                                :shown-thumbnails false
                                :interactions-mode nil}]]

     is-dashboard
     [:div {:class (stl/css :dashboard)}
      [:div {:class (stl/css :dashboard-sidebar)}
       [:> sidebar*
        {:team nil
         :projects []
         :project (:default-project-id profile)
         :profile profile
         :section :dashboard-projects
         :search-term ""}]]])

   children])

(mf/defc exception-page*
  {::mf/props :obj}
  [{:keys [data route] :as props}]

  (let [type        (:type data)
        path        (:path route)
        params      (:query-params route)

        workspace?  (str/includes? path "workspace")
        dashboard?  (str/includes? path "dashboard")
        view?       (str/includes? path "view")

        ;; We store the request access info int this state
        info*       (mf/use-state nil)
        info        (deref info*)

        profile     (mf/deref refs/profile)

        auth-error? (= type :authentication)
        not-found?  (= type :not-found)

        authenticated?
        (is-authenticated? profile)

        request-access?
        (and
         (or workspace? dashboard? view?)
         (or (some? (:file-id info))
             (some? (:team-id info))))]

    (mf/with-effect [params info]
      (when-not (:loaded info)
        (->> (load-info params)
             (rx/subs! (partial reset! info*)
                       (partial reset! info* {:loaded true})))))


    (if (or auth-error? not-found?)
      (if (not authenticated?)
        [:> context-wrapper*
         {:is-workspace workspace?
          :is-dashboard dashboard?
          :is-viewer view?
          :profile profile}
         [:> login-modal* {}]]
        (when (get info :loaded false)
          (if request-access?
            [:> context-wrapper* {:is-workspace workspace?
                                  :is-dashboard dashboard?
                                  :is-viewer view?
                                  :profile profile}
             [:> request-access* {:file-id (:file-id info)
                                  :team-id  (:team-id info)
                                  :is-default (:team-default info)
                                  :profile profile
                                  :is-workspace workspace?}]]
            [:> exception-section* props])))

      [:> exception-section* props])))
